﻿// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace RoslynPad.Roslyn.BraceMatching;

public static class BraceMatchingServiceExtensions
{
    public static async Task<TextSpan?> FindMatchingSpanAsync(
        this IBraceMatchingService service,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        // These are the matching spans when checking the token to the right of the position.
        var braces1 = await service.GetMatchingBracesAsync(document, position, cancellationToken).ConfigureAwait(false);

        // These are the matching spans when checking the token to the left of the position.
        BraceMatchingResult? braces2 = null;

        // Ensure caret is valid at left of position.
        if (position > 0)
        {
            braces2 = await service.GetMatchingBracesAsync(document, position - 1, cancellationToken).ConfigureAwait(false);
        }

        // Favor matches where the position is on the outside boundary of the braces. i.e. if we
        // have:  {^()}  
        //
        // then this would return the  ()  not the  {}
        if (braces1.HasValue && position >= braces1.Value.LeftSpan.Start && position < braces1.Value.LeftSpan.End)
        {
            // ^{ } -- return right span
            return braces1.Value.RightSpan;
        }

        if (braces2.HasValue && position > braces2.Value.RightSpan.Start && position <= braces2.Value.RightSpan.End)
        {
            // { }^ -- return left span
            return braces2.Value.LeftSpan;
        }

        if (braces2.HasValue && position > braces2.Value.LeftSpan.Start && position <= braces2.Value.LeftSpan.End)
        {
            // {^ } -- return right span
            return braces2.Value.RightSpan;
        }

        if (braces1.HasValue && position >= braces1.Value.RightSpan.Start && position < braces1.Value.RightSpan.End)
        {
            // { ^} - return left span
            return braces1.Value.LeftSpan;
        }

        return null;
    }

    /// <summary>
    /// Given code like   ()^()  (where ^ is the caret position), returns the two pairs of
    /// matching braces on the left and the right of the position.  Note: a brace matching
    /// pair is only returned if the position is on the left-side of hte start brace, or the
    /// right side of end brace.  So, for example, if you have (^()), then only the inner 
    /// braces are returned as the position is not on the right-side of the outer braces.
    /// 
    /// This function also works for multi-character braces i.e.  ([  ])   In this case,
    /// the rule is that the position has to be on the left side of the start brace, or 
    /// inside the start brace (but not at the end).  So,    ^([   ])  will return this
    /// as a brace match, as will  (^[    ]).  But   ([^   ])  will not.
    /// 
    /// The same goes for the braces on the the left of the caret.  i.e.:   ([   ])^
    /// will return the braces on the left, as will   ([   ]^).  But   ([   ^]) will not.
    /// </summary>
    public static async Task<(BraceMatchingResult? leftOfPosition, BraceMatchingResult? rightOfPosition)> GetAllMatchingBracesAsync(
        this IBraceMatchingService service,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        // These are the matching spans when checking the token to the right of the position.
        var rightOfPosition = await service.GetMatchingBracesAsync(document, position, cancellationToken).ConfigureAwait(false);

        // The braces to the right of the position should only be added if the position is 
        // actually within the span of the start brace.  Note that this is what we want for
        // single character braces as well as multi char braces.  i.e. if the user has:
        //
        //      ^{ }    // then { and } are matching braces.
        //      {^ }    // then { and } are not matching braces.
        //
        //      ^<@ @>  // then <@ and @> are matching braces.
        //      <^@ @>  // then <@ and @> are matching braces.
        //      <@^ @>  // then <@ and @> are not matching braces.
        if (rightOfPosition.HasValue &&
            !rightOfPosition.Value.LeftSpan.Contains(position))
        {
            // Not a valid match.  
            rightOfPosition = null;
        }

        if (position == 0)
        {
            // We're at the start of the document, can't find braces to the left of the position.
            return (leftOfPosition: null, rightOfPosition);
        }

        // See if we're touching the end of some construct.  i.e.:
        //
        //      { }^
        //      <@ @>^
        //      <@ @^>
        //
        // But not
        //
        //      { ^}
        //      <@ ^@>

        var leftOfPosition = await service.GetMatchingBracesAsync(document, position - 1, cancellationToken).ConfigureAwait(false);

        if (leftOfPosition.HasValue &&
            position <= leftOfPosition.Value.RightSpan.End &&
            position > leftOfPosition.Value.RightSpan.Start)
        {
            // Found a valid pair on the left of us.
            return (leftOfPosition, rightOfPosition);
        }

        // No valid pair of braces on the left of us.
        return (leftOfPosition: null, rightOfPosition);
    }
}
